package com.travel.carmen;

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.xmlrpc.*;

import android.util.Log;


/**
	XMLRPC Service class for the Travel Mobile Application Carmen.
	 This is used to sync data between the Carmen (Drupal) Website
	 and the device through XMLRPC Service requests. An XMLRPC Service
	 module has been installed and setup on the Carmen website to 
	 facilitate this method of carrying data back and forth.
	@author Arturo Aparicio 
	@author Edwin Boykin 
	@author Veena Chengalnaidu
	@author Anton Ganeshalingam
	@author Lakshmi Mallampati
	@author Tarun Morton
	@author Chantha Thoeun
*/
public class DrupalXmlRpcService {

        /**
         * Method names
         */
        public static final String MethodNodeGet = "node.get";
        public static final String MethodNodeSave = "node.save";
        public static final String MethodNodeDelete = "node.delete";
        public static final String MethodSystemConnect = "system.connect";
        public static final String MethodUserLogout = "user.logout";
        public static final String MethodUserLogin = "user.login";
        public static final String MethodViewsGet = "views.get";
        
        private String username;
        private String password;
        
        Logger log = Logger.getLogger(DrupalXmlRpcService.class.getName());
        
        final String serviceURL;
        final String serviceDomain;
        final String apiKey;
        
        String sessionID;
        XMLRPCClient xmlRpcClient;
        
        final Charset asciiCs = Charset.forName("US-ASCII");
        
        Mac apikeyMac;
        
        
        /**
         * Initialize the Drupal XML-RPC Service.
         * The serviceURL must be a valid URL.
         * @param serviceDomain domain
         * @param apiKey the API key with the domain
         * @param serviceURL the URL for the Drupal service.
         * @throws MalformedURLException if the serviceURL is not a URL.
         */
        public DrupalXmlRpcService(String serviceDomain, String apiKey, 
                        String serviceURL) throws MalformedURLException {
                this.serviceDomain = serviceDomain;
                this.apiKey = apiKey;
                this.serviceURL = serviceURL;
                
                // initialize the xmlRpcClient, it won't change as the parameters
                // are final.
                xmlRpcClient = new XMLRPCClient(new URL(this.serviceURL));
        }
        
        
        /**
         * Generate a random string for signing methods
         * @return a string representation of the current 
         *  time stamp in milliseconds
         */
        private String generateNonce() {
                return "" + System.currentTimeMillis();
        }
        
        
        /**
         * Generate the default parameters that need to precede every call.
         * @param methodname pass in the method name which should be signed.
         * @return the default parameters. The Vector can be reused to pass
         * other objects via XML_RPC, hence the resulting vector is typed to
         * "Object" rather than String.
         */
        private Vector<Object> generateDefaultParams(String methodname) 
                throws Exception {
                String nonce = generateNonce();
                long timestamp = System.currentTimeMillis();
                // Build String for hashing.
                String hashString = Long.toString(timestamp) + ";" + serviceDomain +
                        ";" + nonce + ";" + methodname;
                String hash = generateHmacHash(hashString);
                
                Vector<Object> params = new Vector<Object>();
                
                params.add(hash);
                params.add(this.serviceDomain);
                params.add(Long.toString(timestamp));
                params.add(nonce);
                params.add(this.sessionID);
                return params;
        }
        
        
        /**
         * Compute the HMAC-SHA256 hash of the passed message.
         * As key, the API key is used.
         * @param message the message to hash
         * @return the hash as hex-encoded string.
         * @throws Exception if the encoding algorithm HmacSHA256 can't
         * be found or other problems arise.
         */
        public String generateHmacHash(String message) throws Exception {
                byte[] hash = getApikeyMac().doFinal(asciiCs.encode(message).array());
                
                String result = "";
                for (int i = 0; i < hash.length; i++) {
                        result += Integer.toString((hash[i] & 0xff) + 0x100, 16).substring(1);
                }
                log.finest("Created HMAC: " + result + " of " + message);
                return result;
        }
        
        
        /**
         * Getter for HMAC, as this is used on every call.
         * @return apikey MAC
         */
        private Mac getApikeyMac() throws Exception {
                if (apikeyMac == null) {
                        SecretKeySpec keySpec = 
                                new javax.crypto.spec.SecretKeySpec(asciiCs.encode(
                                                this.apiKey).array(), "HmacSHA256");
                        apikeyMac = javax.crypto.Mac.getInstance("HmacSHA256");
                        apikeyMac.init(keySpec);
                }
                return apikeyMac;
        }
        
        
        /**
         * Connect to the remove service
         * @return true if connected successfully, else false
         * @throws Exception if connection failed
         */
        public boolean connect() {
                try {
                        HashMap<?, ?> map = (HashMap<?, ?>)xmlRpcClient.callEx(MethodSystemConnect, new Object[] {});
                        this.sessionID = (String) map.get("sessid");
                        System.out.println(this.sessionID);
                        log.fine("Connected to server using SessionID: " + this.sessionID);
                        return true;
                } 
                catch (Exception ex) {
                        System.err.println("cannot connect to " + serviceURL + ":"
                                        + ex.getMessage());
                }
                return false;
        }
        
        
        /**
         * Call user.login
         * @param username Carmen account username
         * @param password Corresponding account password
         * @return true if logged user in successfully, else false
         */
        public boolean login(String username, String password) {
        	
                try {
                        Vector<Object> params = generateDefaultParams(MethodUserLogin);
                        // Add Login parameters
                        params.add(username);
                        params.add(password);
                        
                        this.username = username;
                        this.password = password;
                        
                        HashMap<?, ?> o = (HashMap<?, ?>)xmlRpcClient.callEx(MethodUserLogin, params.toArray(new Object[]{}));
                        // the login changes the session id. The new session ID is
                        // authorized, the old one not.
                        this.sessionID = (String) o.get("sessid");

                        log.fine("Successful Login");
                        return true;
                }
                catch (Exception ex) {
                        System.err.println(ex.toString());
                        Log.d("[LOGIN]", "FAILED");
                }
                return false;
        }
        
        
        /**
         * Call user.logout
         * @return true if logged out user successfully, else false.
         */
        public boolean logout() {
                try {
                        Vector<Object> params = generateDefaultParams(MethodUserLogout);
                        params.add(this.sessionID);
                        xmlRpcClient.callEx(MethodUserLogout, params.toArray(new Object[]{}));
                        log.finer("Logout Successful");
                        return true;
                }
                catch (Exception ex) {
                        System.err.println(ex.toString());
                }
                return false;
        }
        
        
        /**
         * Call node.save
         * @param node the node to save
         * @return true if node was saved successfully, else false
         */
        public boolean nodeSave(DrupalNode node) {
                try {
                        Vector<Object> params = generateDefaultParams(MethodNodeSave);
                        params.add(node);
                        Object o = xmlRpcClient.callEx(MethodNodeSave, params.toArray(new Object[]{}));
                        if (log.isLoggable(Level.FINEST))
                                log.finest(MethodNodeSave + " returned " + o.toString());
                        return true;
                }
                catch (Exception ex) {
                        System.err.println(ex.toString());
                }
                return false;
        }
        
        
        
        /**
         * Call node.delete
         * @param nid the node id to delete
         * @return true if deleted node successfully, else false.
         * @throws Exception if generating parameters failed or calling the method 
         *  throws an XMLRPC Exception
         */
        public boolean nodeDelete(int nid) throws Exception
    	{
        	try 
        	{
        		Vector<Object> params = generateDefaultParams(MethodNodeDelete);
        		params.add(nid);
        		xmlRpcClient.callEx(MethodNodeDelete, params.toArray());
        		 if (log.isLoggable(Level.FINEST))
                     log.finest(MethodNodeDelete + " deleted NID " + nid);
        		 return true;
        	}
        	catch (Exception ex) 
        	{
                System.err.println(ex.toString());
        	}
        	return false;
    	}
        
        
        /**
         * Call node.get
         * @param nid the Node ID to get
         * @return a DrupalNode matching the requested Node ID
         */
        public Object nodeGet(int nid) {
                try {
                        Vector<Object> params = generateDefaultParams(MethodNodeGet);
                        params.add(nid);
                        Object o = xmlRpcClient.callEx(MethodNodeGet, params.toArray());
                        if (log.isLoggable(Level.FINEST))
                                log.finest(MethodNodeGet + " returned " + o.toString());
                        
                        System.out.println(o.toString());
                        return o;
                }
                catch (Exception ex) {
                        System.out.println(ex.getClass().toString());
                        System.out.println(ex.toString());
                }
                return null;
        }
        
        
        /**
           Call view.get
           @param view the admin created view type on the Carmen (Drupal) Website
           @param name an optional parameter
         * @return a HashMap of the view with key and value of type String
         * @throws Exception if calling the method throws an XMLRPCException
         */
        public HashMap<String, String>[] viewsGet(String view, String name) 
                throws Exception 
        {
                Vector<Object> params = generateDefaultParams(MethodViewsGet);
                params.add(view);               
                params.add(name);
                Object o = xmlRpcClient.callEx(MethodViewsGet, params.toArray());
                Object[] oArray = (Object[])o;
                
                HashMap<String, String>[] results = new HashMap[oArray.length];
                
                for(int i = 0; i < oArray.length; i++)
                {
                	if(oArray[i] instanceof HashMap<?,?>)
                		results[i] = (HashMap<String, String>) oArray[i];
                }
                
                return results;
                        
        }
        
        
        /**
           Call view.get (Special method for retrieving Carmen notes)
           @param view the admin created view type on the Carmen (Drupal) Website
           @param name an optional parameter
         * @return a HashMap of the view with key of type String and value of type
         *  Object
         * @throws Exception if calling the method throws an XMLRPCException
         */
        public HashMap<String, Object>[] viewsGetNotes(String view, String name) 
        throws Exception 
        {
	        Vector<Object> params = generateDefaultParams(MethodViewsGet);
	        params.add(view);               
	        params.add(name);
	        Object o = xmlRpcClient.callEx(MethodViewsGet, params.toArray());
	        Object[] oArray = (Object[])o;
	        
	        HashMap<String, Object>[] contents = new HashMap[oArray.length];
	        
	        for(int x = 0; x < oArray.length; x++)
	        {
	        	if(oArray[x] instanceof HashMap<?,?>)
	        		contents[x] = (HashMap<String, Object>) oArray[x];
	        }
	        
	        return contents;
        }
}       